// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Transactions;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    internal class MapperChangeTracker : IMapperChangeTracker
    {
        private readonly IMapperContext _mapperContext;
        private readonly IDictionary<object, IMapperTracker> _mapperTrackers = new Dictionary<object, IMapperTracker>();

        public MapperChangeTracker(IMapperContext mapperContext)
        {
            _mapperContext = mapperContext;
        }

        public IModelEntry GetModelEntry(object model)
        {
            if (_mapperTrackers.TryGetValue(model, out var tracker))
            {
                if (tracker is IModelEntry modelEntry)
                {
                    return modelEntry;
                }
            }

            return default;
        }

        public void Track(Action<ISaveContext> saveAction)
        {
            _mapperTrackers.Add(Guid.NewGuid(), new ActionTracker(saveAction));
        }

        public void Track(IMapperTrackable mapperTrackable, params object[] saveParms)
        {
            var updatableObject = new UpdatableObject(mapperTrackable, saveParms);
            _mapperTrackers.Add(Guid.NewGuid(), new OtherTracker(updatableObject));
        }

        public TModel Track<TModel>(
            IModelEntry<TModel> modelEntry,
            bool changedOnly,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            var current = default(TModel);

            if (modelEntry.ModelState != ModelState.NotTracked)
            {
                var tracker = new EntryTracker<TModel>(_mapperContext, modelEntry,
                                            changedOnly, beforeSaveAction, afterSaveAction);
                current = tracker.Current;

                _mapperTrackers.Add(current, tracker);
            }

            return current;
        }

        public void AddTracker<TModel>(TModel model, IMapperTracker tracker, bool isCascadeModel = false)
        {
            if (model == null || tracker == null)
            {
                return;
            }

            if (isCascadeModel)
            {
                //TODO...
            }
            else
            {
                if (_mapperTrackers.ContainsKey(model))
                {
                    throw new InvalidOperationException("The model is already tracked");
                }
                else
                {
                    _mapperTrackers.Add(model, tracker);
                }
            }
        }

        public void TrackCreate<TModel>(
            TModel model,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            if (_mapperTrackers.ContainsKey(model))
            {
                throw new InvalidOperationException("The model is already tracked");
            }
            else
            {
                _mapperTrackers.Add(model, new CreateTracker<TModel>(
                                                _mapperContext, model, beforeSaveAction, afterSaveAction));
            }
        }

        public void TrackUpdate<TModel>(
            TModel model,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            if (_mapperTrackers.ContainsKey(model))
            {
                throw new InvalidOperationException("The model is already tracked");
            }
            else
            {
                var updateTracker = new UpdateTracker<TModel>(
                                            _mapperContext, model, beforeSaveAction, afterSaveAction);
                _mapperTrackers.Add(model, updateTracker);
            }
        }

        public void TrackUpdate<TModel>(
            TModel originalModel,
            TModel modifiedModel,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            if (_mapperTrackers.ContainsKey(originalModel) ||
                    _mapperTrackers.ContainsKey(modifiedModel))
            {
                throw new InvalidOperationException("The model is already tracked");
            }
            else if (originalModel.Equals(modifiedModel))
            {
                throw new InvalidOperationException("The original model and the modified model cannot be the same");
            }
            else
            {
                var updateTracker = new UpdateTracker<TModel>(_mapperContext, originalModel,
                            modifiedModel, beforeSaveAction, afterSaveAction, false);

                _mapperTrackers.Add(modifiedModel, updateTracker);
            }
        }

        public void TrackDelete<TModel>(
            TModel model,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            if (_mapperTrackers.ContainsKey(model))
            {
                throw new InvalidOperationException("The model is already tracked.");
            }
            else
            {
                _mapperTrackers.Add(model, new DeleteTracker<TModel>(
                                                _mapperContext, model, beforeSaveAction, afterSaveAction));
            }
        }

        public void TrackSqlCUD(string sqlText, params object[] parameters)
        {
            _mapperTrackers.Add(Guid.NewGuid(), new SqlTracker(sqlText, parameters));
        }

        public void TrackSqlCUD(DataContext context, ISqlInsertBuilder insertBuilder, params object[] parameters)
        {
            _mapperTrackers.Add(Guid.NewGuid(), new SqlTracker(context, insertBuilder, parameters));
        }

        public void TrackSqlCUD(DataContext context, ISqlDeleteBuilder deleteBuilder, params object[] parameters)
        {
            _mapperTrackers.Add(Guid.NewGuid(), new SqlTracker(context, deleteBuilder, parameters));
        }

        public void TrackSqlCUD(DataContext context, ISqlUpdateBuilder updateBuilder, params object[] parameters)
        {
            _mapperTrackers.Add(Guid.NewGuid(), new SqlTracker(context, updateBuilder, parameters));
        }

        public IDbResult Update(DataContext context)
        {
            IDbResult result = null;

            var hasError = false;
            var batchExecute = context.ContextOptions.BatchExecuting;
            IAdoDbTransaction transaction = null;

            if (context.CurrentTransaction == null && Transaction.Current == null)
            {
                transaction = context.BeginTransaction();
            }

            try
            {
                result = this.UpdateInternal(context, batchExecute);
            }
            catch
            {
                hasError = true;
                throw;
            }
            finally
            {
                if (transaction != null)
                {
                    if (hasError || result.Cancelled)
                    {
                        context.Rollback();
                    }
                    else
                    {
                        context.Commit();
                    }
                }

                _mapperTrackers.Clear();
            }

            return result;
        }

        public async Task<IDbResult> UpdateAsync(DataContext context, CancellationToken cancellationToken)
        {
            IDbResult result = null;

            var hasError = false;
            var batchExecute = context.ContextOptions.BatchExecuting;
            IAdoDbTransaction transaction = null;

            if (context.CurrentTransaction == null && Transaction.Current == null)
            {
                transaction = await context.BeginTransactionAsync(cancellationToken);
            }

            try
            {
                result = await this.UpdateInternalAsync(context, batchExecute, cancellationToken);
            }
            catch
            {
                hasError = true;
                throw;
            }
            finally
            {
                if (transaction != null)
                {
                    if (hasError || result.Cancelled)
                    {
                        context.Rollback();
                    }
                    else
                    {
                        context.Commit();
                    }
                }

                _mapperTrackers.Clear();
            }

            return result;
        }

        private IDbResult UpdateInternal(
            DataContext context,
            bool batchExecute)
        {
            IDbResult result = null;

            var updatables = this.GetUpdatables();

            if (batchExecute)
            {
                result = UpdateBatchExecutor.Instance.Update(context, updatables);
            }
            else
            {
                result = UpdateSingularExecutor.Instance.Update(context, updatables);
            }

            return result;
        }

        private async Task<IDbResult> UpdateInternalAsync(
            DataContext context,
            bool batchExecute,
            CancellationToken cancellationToken)
        {
            IDbResult result = null;

            var updatables = this.GetUpdatables();

            if (batchExecute)
            {
                result = await UpdateBatchExecutor.Instance.UpdateAsync(context, updatables, cancellationToken);
            }
            else
            {
                result = await UpdateSingularExecutor.Instance.UpdateAsync(context, updatables, cancellationToken);
            }

            return result;
        }

        private IReadOnlyList<IUpdatable> GetUpdatables()
        {
            var updatables = new List<IUpdatable>();

            foreach (var mapperTracker in _mapperTrackers)
            {
                var tracker = mapperTracker.Value;

                if (tracker.TrackerType == TrackerType.Model ||
                     tracker.TrackerType == TrackerType.ModelEntry)
                {
                    var modelTracker = (IModelTracker)tracker;
                    var trackedModels = modelTracker.GetUpdateableModels();

                    foreach (var trackedModel in trackedModels)
                    {
                        if (trackedModel.ModelState == ModelState.NewModified ||
                            trackedModel.ModelState == ModelState.Modified)
                        {
                            var validationResults = modelTracker.Validate();

                            if (validationResults.Count > 0)
                            {
                                throw new ValidationException(validationResults.FirstOrDefault().ErrorMessage);
                            }
                            else
                            {
                                updatables.Add(trackedModel);
                            }
                        }
                        else if (trackedModel.ModelState == ModelState.Deleted ||
                            trackedModel.ModelState == ModelState.NotModified) //IgnoreSave, to support master/detail track SetValue attribute
                        {
                            updatables.Add(trackedModel);
                        }
                    }
                }
                else if (tracker.TrackerType == TrackerType.RawSql)
                {
                    var sqlTracker = (ISqlTracker)tracker;
                    updatables.Add(sqlTracker.UpdatableSql);
                }
                else if (tracker.TrackerType == TrackerType.Action)
                {
                    var actionTracker = (IActionTracker)tracker;
                    updatables.Add(actionTracker.UpdatableAction);
                }
                else if (tracker.TrackerType == TrackerType.OtherTrackable)
                {
                    var otherTracker = (IOtherTracker)tracker;
                    updatables.Add(otherTracker.UpdatableObject);
                }
            }

            return updatables;
        }

        public IList<ValidationResult> ValidateAll()
        {
            var results = new List<ValidationResult>();

            foreach (var tracker in _mapperTrackers)
            {
                if (tracker.Value.TrackerType == TrackerType.Model ||
                     tracker.Value.TrackerType == TrackerType.ModelEntry)
                {
                    var modelTracker = (IModelTracker)tracker.Value;

                    var masterModel = modelTracker.GetMasterModel();

                    if (masterModel != null && masterModel.ModelState == ModelState.NewModified)
                    {
                        results.AddRange(modelTracker.Validate());
                    }
                    else
                    {
                        var trackedModels = modelTracker.GetUpdateableModels();

                        foreach (var trackedModel in trackedModels)
                        {
                            if (trackedModel.ModelState == ModelState.NewModified ||
                                trackedModel.ModelState == ModelState.Modified)
                            {
                                results.AddRange(modelTracker.Validate());
                            }
                        }
                    }
                }
            }

            return results;
        }

        public bool RemoveTrackedModel<TModel>(TModel model)
        {
            if (model != null && _mapperTrackers.ContainsKey(model))
            {
                return _mapperTrackers.Remove(model);
            }

            return false;
        }

        public void RemoveAllTracked()
        {
            _mapperTrackers.Clear();
        }
    }
}
